/*
	Copyright (C) 2011 Salil Bhagurkar

	This file is part of illusion

	illusion is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as
	published by the Free Software Foundation, either version 3 of
	the License, or (at your option) any later version.

	illusion is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with illusion. 
	If not, see <http://www.gnu.org/licenses/>.
*/

#include <klib/lib.h>
#include <klib/memory.h>
#include <klib/string.h>
#include <arch/arch.h>
#include <arch/power.h>
#include <kernel/errors.h>
#include <kernel/console.h>
#include <kernel/graphics.h>
#include <kernel/kmalloc.h>
#include <kernel/list.h>
#include <kernel/vmpage.h>
#include <kernel/page.h>
#include <kernel/init.h>

/* SuperVGA information block */
struct vbeinfo
{
	unsigned char		VESASignature[4];       /* 'VESA' 4 byte signature          */
	unsigned short		VESAVersion;            /* VBE version number               */
	unsigned long		OEMStringPtr;      /* Pointer to OEM string            */
	unsigned long		Capabilities;           /* Capabilities of video card       */
	unsigned long		VideoModePtr;     /* Pointer to supported modes       */
	unsigned short		TotalMemory;            /* Number of 64kb memory blocks     */
	unsigned short		oemver;
	unsigned long		vendorname;
	unsigned long		productname;
	unsigned char		productrevision;
	unsigned short 		vbeafver;
	unsigned long		accelmodes;
	unsigned char		vbe[216];
	unsigned char		oemscratch[256];
}__attribute__((packed));

/* SuperVGA mode information block */
struct vbemodeinfo
{
    unsigned short ModeAttributes;      /* Mode attributes                 */
    unsigned char  WinAAttributes;      /* Window A attributes             */
    unsigned char  WinBAttributes;      /* Window B attributes             */
    unsigned short WinGranularity;      /* Window granularity in k         */
    unsigned short WinSize;             /* Window size in k                */
    unsigned short WinASegment;         /* Window A segment                */
    unsigned short WinBSegment;         /* Window B segment                */
    void (*WinFuncPtr)(void);       /* Pointer to window function      */
    unsigned short BytesPerScanLine;    /* Bytes per scanline              */
    unsigned short XResolution;         /* Horizontal resolution           */
    unsigned short YResolution;         /* Vertical resolution             */
    unsigned char  XCharSize;           /* Character cell width            */
    unsigned char  YCharSize;           /* Character cell height           */
    unsigned char  NumberOfPlanes;      /* Number of memory planes         */
    unsigned char  BitsPerPixel;        /* Bits per pixel                  */
    unsigned char  NumberOfBanks;       /* Number of CGA style banks       */
    unsigned char  MemoryModel;         /* Memory model type               */
    unsigned char  BankSize;            /* Size of CGA style banks         */
    unsigned char  NumberOfImagePages;  /* Number of images pages          */
    unsigned char  res1;                /* Reserved                        */
    unsigned char  RedMaskSize;         /* Size of direct color red mask   */
    unsigned char  RedFieldPosition;    /* Bit posn of lsb of red mask     */
    unsigned char  GreenMaskSize;       /* Size of direct color green mask */
    unsigned char  GreenFieldPosition;  /* Bit posn of lsb of green mask   */
    unsigned char  BlueMaskSize;        /* Size of direct color blue mask  */
    unsigned char  BlueFieldPosition;   /* Bit posn of lsb of blue mask    */
    unsigned char  RsvdMaskSize;        /* Size of direct color res mask   */
    unsigned char  RsvdFieldPosition;   /* Bit posn of lsb of res mask     */
    unsigned char  DirectColorModeInfo; /* Direct color mode attributes    */
    unsigned long phys_base;
    unsigned long offscreen_start;
    unsigned short offscreen_memory;
    unsigned short bytesperscanlin;
    unsigned char n_image_banked;
    unsigned char n_image_linear;
    unsigned char lin_red_mask;
    unsigned char lin_red_mask_shift;
    unsigned char lin_green_mask;
    unsigned char lin_green_mask_shift;
    unsigned char lin_blue_mask;
    unsigned char lin_blue_mask_shift;
    unsigned char lin_res_mask;
    unsigned char lin_res_mask_shift;
    unsigned long max_pixel_clock;
    unsigned char reserved[190];
} __attribute__((packed));



//Mode information block, pointing to the actual in the bios.. Please make local copies of this.
static struct vbemodeinfo *ModeInfoBlock;
//VBE full info block
static struct vbeinfo *VbeInfoBlock;
//Physical base address of the current mode
static unsigned char *phys_base;
//Current bank
static int curbank;
static int bankshift;
static int current_mode;
static int prev_mode;


int vbe_get_info()
{
	struct bioscall_regs regs={0};
	VbeInfoBlock=(void *)0x8000;
	regs.eax=0x4f00;
	regs.ebx=0;
	regs.ecx=0;
	regs.edx=0;
	regs.esi=0;
	regs.edi=(unsigned long)VbeInfoBlock;
	regs.f=0x10;
	bios_call(&regs);
	if(((unsigned short)regs.eax)!=0x4f)
		return ENOSUP;
	return 0;
}

static int vbe_get_mode_info(int mode)
{
	struct bioscall_regs regs;
	ModeInfoBlock=(void *)0x8100;
	if (mode < 0x100) return ENOSUP;

	regs.eax=0x4f01;
	regs.ecx=mode;
	regs.edi=(unsigned long)ModeInfoBlock;
	regs.f=0x10;
	bios_call(&regs);

	if (regs.eax != 0x4F) return ENOSUP;
	return 0;
}

/* Set a VBE video mode */
static int vbe_set_mode(int mode)
{
	struct bioscall_regs regs;
	regs.eax=0x4f03;
	regs.ebx=0;
	regs.f=0x10;
	bios_call(&regs);
	if((unsigned char)(regs.eax)!=0x4f) { //Should this screw up?? (checking prev mode is not so important)
		return ENOSUP;
	}
	prev_mode=regs.ebx;
	debug("\nvbe: Setting mode:0x%x", mode);
	regs.eax = 0x4F02; regs.ebx = mode;
	regs.f=0x10;
	bios_call(&regs);
	if((unsigned char)(regs.eax)!=0x4f) {
		return ENOSUP;
	}
	current_mode=mode;
	return 0;
}

static int vbe_reset_mode()
{
	struct bioscall_regs regs;
	regs.eax = 0x3; regs.ebx = 0;
	regs.f=0x10;
	bios_call(&regs);
	current_mode=prev_mode;
	//Free the allocated virtual space
	//'phys_base' is actually the virtual space that was allocated for the physical address.
	unsigned long size = ModeInfoBlock->XResolution * ModeInfoBlock->YResolution
						* (ModeInfoBlock->BitsPerPixel / 8);
	vm_free_virt(&kernel_vmmap, phys_base, size / PAGE_SIZE);
	return 0;
}

/* Return the current VBE video mode */
static int vbe_get_mode(void)
{
    struct bioscall_regs regs;
    regs.eax=0x4f03;
	regs.f=0x10;
    bios_call(&regs);
	if(((unsigned short)regs.eax)!=0x4f)
		return ENOSUP;
    return regs.ebx;
}

/*The video buffer generally starts at 0xa0000 (0xa000:0x0000). But you can access only
64 kB of it since the length of a segment is 64 kB. So VESA provides a function which allows
you to write in different locations of the screen with the same video memory..
You just need to change the bank according to your need.

Further vbe has some LFBs thats Linear frame buffers. Which are only accessible in pmode
and are of the range 0xe0000000-0xf0000000... They can be directly accessed without
a bank change..*/

/* Set new read/write bank. Set both Window A and Window B, as many VBE's have
 * these set as separately available read and write windows. We also use a
 * simple (but very effective) optimization of checking if the requested bank
 * is currently active. */
static int vbe_set_bank(unsigned long bank)
{
    struct bioscall_regs regs;
	if(bank > 48 || bank < 0 )
		return EPARAM;
    if (bank == curbank) return 0;    /* Bank is already active           */
    curbank = bank;                 /* Save current bank number         */
    bank <<= bankshift;             /* Adjust to window granularity     */
    regs.eax = 0x4F05;
    regs.ebx = 0;
    regs.edx = bank;
	regs.f=0x10;
    bios_call(&regs);
    regs.eax = 0x4F05;
    regs.ebx = 1;
    regs.edx = bank;
	regs.f=0x10;
    bios_call(&regs);
	return 0;
}

/*A slow putpixel... For gui thingies you never use putpixel... you use memory routines
that allow fast transfers..*/
static void vbe_putpixel(unsigned long x,unsigned long y,unsigned long color)
{
    unsigned long addr = (unsigned long)y * ModeInfoBlock->BytesPerScanLine + x*(ModeInfoBlock->BitsPerPixel)/8;
    vbe_set_bank((unsigned short)(addr >> 16));
    switch(ModeInfoBlock->BitsPerPixel)	{
    	case 32:
    	*(phys_base + (addr & 0xFFFF)) = (char)(color);
    	*(phys_base + (addr & 0xFFFF)+1) = (char)(color>>8);
    	*(phys_base + (addr & 0xFFFF)+2) = (char)(color>>16);
    	*(phys_base + (addr & 0xFFFF)+3) = (char)(color>>24);
    	break;
	case 16:
	*(phys_base + (addr & 0xFFFF))=(char)(color);
	*(phys_base + (addr & 0xFFFF)+1)=(char)(color >> 8);
	break;
    	case 8:
    	*(phys_base + (addr & 0xFFFF)) = (char)(color);
    	break;
    }
}
/*Gets color of a pixel*/
static unsigned long vbe_getpixel(unsigned long x,unsigned long y)
{
	unsigned long *ph,addr = (unsigned long)y * ModeInfoBlock->BytesPerScanLine + x*(ModeInfoBlock->BitsPerPixel)/8;
	vbe_set_bank((int)(addr >> 16));
	ph=(unsigned long *)phys_base;
	return *(ph+addr/((ModeInfoBlock->BitsPerPixel)/8));
}

/*These fns are relatively fast functions for Linear Frame Buffer Modes*/
void vbe_putlfb(unsigned long x,unsigned long y,unsigned long color)
{
	unsigned long *ph,addr=(unsigned long)y * ModeInfoBlock->BytesPerScanLine + x*(ModeInfoBlock->BitsPerPixel)/8;
	ph=(unsigned long *)phys_base;
	*(ph+addr/((ModeInfoBlock->BitsPerPixel)/8))=color;
}

unsigned long vbe_getlfb(unsigned long x,unsigned long y)
{
	unsigned long *ph,addr=(unsigned long)y * ModeInfoBlock->BytesPerScanLine + x*(ModeInfoBlock->BitsPerPixel)/8;
	ph=(unsigned long *)phys_base;
	return *(ph+addr/((ModeInfoBlock->BitsPerPixel)/8));
}

static unsigned long flat(unsigned long ptr)
{
	unsigned short seg,off;
	unsigned long flat;
	off=(unsigned short)(ptr);
	seg=(unsigned short)((ptr)>>16);
	flat=seg*0x10+off;
	return flat;
}


enum {MM_TEXT=0,MM_CGA,MM_HGC,MM_EGA16,MM_PACKED,MM_SEQU256,MM_DIRECT,MM_YUV};

char *memory_models[]={"text","cga","hgc","ega_16_color","packed","sequ_256","direct_color","yuv"};

void print_mode_info(int mode)
{
	unsigned short attr;
	vbe_get_mode_info(mode);
	debug("%ux%u(%x)%ubpp\n",ModeInfoBlock->XResolution,ModeInfoBlock->YResolution,mode,ModeInfoBlock->BitsPerPixel);
	attr=ModeInfoBlock->ModeAttributes;

	if(attr & 1)
		debug(":supported");
	if(attr & (1<<3))
		debug(":color");
	else
		debug(":monochrome");
	if(!(attr & (1<<4)))
		debug(":text");
	if(attr & (1<<6))
		debug(":noBSW");
	if(attr & (1<<7))
		debug(":lfb");
	debug("memory_model=");
	if(ModeInfoBlock->MemoryModel < 0x8)
		debug("%s",memory_models[ModeInfoBlock->MemoryModel]);
	else if(ModeInfoBlock->MemoryModel < 0x10)
		debug("reserved_vesa");
	else
		debug("oem");
	debug(",bpsl=%u,bpp=%u\n",ModeInfoBlock->BytesPerScanLine,ModeInfoBlock->BitsPerPixel);
}


static int vbe_init_mode(struct graphics_mode *g_mode)
{
	unsigned short mode = (unsigned short)g_mode->mode_id;
	unsigned long i;
	unsigned short attr;
	if(mode > 0x5000) {
		return EPARAM;
	}
	i=vbe_get_mode_info(mode);
	if(!i && (ModeInfoBlock->ModeAttributes & 0x81)==0x81 && ModeInfoBlock->MemoryModel==MM_DIRECT)	{
		phys_base=(unsigned char *)0xa0000; //This part is always accessible to the kernel
	    	bankshift = 0;
	        while ((unsigned long)(64 >> bankshift) != ModeInfoBlock->WinGranularity)	{
	        	bankshift++;
	    	}
		if(!vbe_get_mode_info(mode | 0x4000)) { //Set lfb if available
			mode=mode | 0x4000;
			vbe_set_mode(mode);
			phys_base = (unsigned char *)ModeInfoBlock->phys_base;

			unsigned long size = ModeInfoBlock->XResolution * ModeInfoBlock->YResolution
					* (ModeInfoBlock->BitsPerPixel / 8);
			//Enable access to the phys_base by creating a virt for it
			void *virt_base = vm_get_virt_for_phys(&kernel_vmmap, phys_base, size / PAGE_SIZE, PAGE_PRESENT | PAGE_WRITE);
			phys_base = virt_base;

		} else {
			vbe_set_mode(mode);
		}
		current_mode = mode;
	    	return 0;
	}
    return ENOSUP;
}


static int vbe_flush_buffer(void *_buf)
{
	unsigned long *buf = (unsigned long *)_buf;
	unsigned long x, y, count = ModeInfoBlock->YResolution * ModeInfoBlock->XResolution;
	if(current_mode & 0x4000) {
		if(ModeInfoBlock->BitsPerPixel == 32) {
			memcpy(phys_base, buf, count * 4);
		} else {
			for(y=0; y<ModeInfoBlock->YResolution; y++) {
				for(x=0; x<ModeInfoBlock->XResolution; x++) {
					vbe_putlfb(x,y,*(buf++));
				}
			}
		}
	} else {
		for(y=0; y<ModeInfoBlock->YResolution; y++) {
			for(x=0; x<ModeInfoBlock->XResolution; x++) {
				vbe_putpixel(x,y,*(buf++));
			}
		}
	}
	return 0;
}

static struct graphics_driver vbe_driver;

static void add_mode(unsigned long mode_id, int speed_level)
{
	struct graphics_mode *g_mode = (struct graphics_mode *)kmalloc(sizeof(struct graphics_mode), "gmode");
	g_mode->mode_id = mode_id;
	g_mode->x = ModeInfoBlock->XResolution;
	g_mode->y = ModeInfoBlock->YResolution;
	g_mode->bits_per_pixel = ModeInfoBlock->BitsPerPixel;
	g_mode->speed_level = speed_level;
	g_mode->driver = &vbe_driver;
	list_attach(vbe_driver.modes, vbe_driver.modes_tail, g_mode);
}

/*
 * Populate the mode list on the driver
 */
static int vbe_get_modes()
{
	unsigned short *mode;

	for(mode=(unsigned short *)flat(VbeInfoBlock->VideoModePtr);;mode++) {
		if((*mode)==0xffff)
			break;
		int err = vbe_get_mode_info(*mode);
		if(!err)
			add_mode((unsigned long)(*mode), 0);
	}
	return 0;
}

int vbe_init();

static struct graphics_driver vbe_driver = {
		.name = "VBE",
		.modes = null, .modes_tail = null, .current_mode = null,
		.flush_buffer = vbe_flush_buffer,
		.init = vbe_init,
		.get_modes = vbe_get_modes,
		.set_mode = vbe_init_mode,
		.reset_mode = vbe_reset_mode,
};


int vbe_init()
{
	int err = vbe_get_info();
	if(!err) {
		initmsg("Detected VBE version %u\n", VbeInfoBlock->VESAVersion);
		vbe_get_modes();
		register_graphics_driver(&vbe_driver);
		return 0;
	}
	return err;
}
